Tend your corner of the atmosphere. spores.garden turns your AT Protocol records into a personal site with unique themes. Your data never leaves your PDS. Grow something that's truly yours. spores.garden
at main 114 lines 3.0 kB view raw
1/** 2 * Reusable help tooltip (?) button + tooltip popup. 3 * 4 * The tooltip is portalled to document.body so it escapes any 5 * containing blocks created by backdrop-filter / transform / etc. 6 */ 7 8export function createHelpTooltip(text: string, options: { allowHtml?: boolean } = {}): HTMLElement { 9 const wrapper = document.createElement('span'); 10 wrapper.style.position = 'relative'; 11 wrapper.style.display = 'inline-flex'; 12 wrapper.style.alignItems = 'center'; 13 14 const btn = document.createElement('button'); 15 btn.type = 'button'; 16 btn.className = 'did-info-button'; 17 btn.setAttribute('aria-label', 'Help'); 18 btn.textContent = '?'; 19 20 const tooltip = document.createElement('div'); 21 tooltip.className = 'help-tooltip'; 22 tooltip.setAttribute('role', 'tooltip'); 23 if (options.allowHtml) { 24 tooltip.innerHTML = text; 25 } else { 26 tooltip.textContent = text; 27 } 28 tooltip.style.display = 'none'; 29 30 wrapper.appendChild(btn); 31 // Tooltip lives on body, not inside the wrapper 32 document.body.appendChild(tooltip); 33 34 let isVisible = false; 35 let ignoreNextClick = false; 36 const supportsHover = typeof window !== 'undefined' 37 && typeof window.matchMedia === 'function' 38 && window.matchMedia('(hover: hover) and (pointer: fine)').matches; 39 40 const position = () => { 41 const rect = btn.getBoundingClientRect(); 42 const pad = 8; 43 tooltip.style.top = `${rect.bottom + pad}px`; 44 45 const tooltipWidth = tooltip.offsetWidth; 46 let left = rect.right - tooltipWidth; 47 if (left < pad) { 48 left = rect.left; 49 } 50 if (left + tooltipWidth > window.innerWidth - pad) { 51 left = window.innerWidth - pad - tooltipWidth; 52 } 53 tooltip.style.left = `${Math.max(pad, left)}px`; 54 }; 55 56 const show = () => { 57 isVisible = true; 58 btn.setAttribute('aria-expanded', 'true'); 59 tooltip.style.display = 'block'; 60 position(); 61 }; 62 const hide = () => { 63 isVisible = false; 64 btn.setAttribute('aria-expanded', 'false'); 65 tooltip.style.display = 'none'; 66 }; 67 const toggle = () => { 68 if (isVisible) { 69 hide(); 70 return; 71 } 72 show(); 73 }; 74 75 if (supportsHover) { 76 btn.addEventListener('mouseenter', show); 77 btn.addEventListener('mouseleave', hide); 78 } 79 80 if (typeof window !== 'undefined' && 'PointerEvent' in window) { 81 btn.addEventListener('pointerdown', (e) => { 82 if (e.pointerType !== 'mouse') { 83 ignoreNextClick = true; 84 e.preventDefault(); 85 e.stopPropagation(); 86 toggle(); 87 } 88 }); 89 } else { 90 btn.addEventListener('touchstart', (e) => { 91 ignoreNextClick = true; 92 e.preventDefault(); 93 e.stopPropagation(); 94 toggle(); 95 }, { passive: false }); 96 } 97 98 btn.addEventListener('click', (e) => { 99 if (ignoreNextClick) { 100 ignoreNextClick = false; 101 return; 102 } 103 e.stopPropagation(); 104 toggle(); 105 }); 106 107 document.addEventListener('click', (e) => { 108 if (!wrapper.contains(e.target as Node)) { 109 hide(); 110 } 111 }); 112 113 return wrapper; 114}