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
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}