diff --git a/public/channels.js b/public/channels.js index 5dc9bdbe..6e6c8fbe 100644 --- a/public/channels.js +++ b/public/channels.js @@ -51,7 +51,29 @@ if (tip) tip.remove(); } + let _focusTrapCleanup = null; + let _nodePanelTrigger = null; + + function trapFocus(container) { + function handler(e) { + if (e.key === 'Escape') { closeNodeDetail(); return; } + if (e.key !== 'Tab') return; + const focusable = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (!focusable.length) return; + const first = focusable[0], last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { e.preventDefault(); last.focus(); } + } else { + if (document.activeElement === last) { e.preventDefault(); first.focus(); } + } + } + container.addEventListener('keydown', handler); + return function () { container.removeEventListener('keydown', handler); }; + } + async function showNodeDetail(name) { + _nodePanelTrigger = document.activeElement; + if (_focusTrapCleanup) { _focusTrapCleanup(); _focusTrapCleanup = null; } const node = await lookupNode(name); selectedNode = name; @@ -72,6 +94,8 @@
No node record found — this sender has only been seen in channel messages, not via adverts.
`; + _focusTrapCleanup = trapFocus(panel); + panel.querySelector('.ch-node-close')?.focus(); return; } @@ -97,15 +121,24 @@ ` : ''} View full node detail → `; + _focusTrapCleanup = trapFocus(panel); + panel.querySelector('.ch-node-close')?.focus(); } catch (e) { panel.innerHTML = `
${escapeHtml(name)}
Failed to load
`; + _focusTrapCleanup = trapFocus(panel); + panel.querySelector('.ch-node-close')?.focus(); } } function closeNodeDetail() { + if (_focusTrapCleanup) { _focusTrapCleanup(); _focusTrapCleanup = null; } const panel = document.getElementById('chNodePanel'); if (panel) panel.classList.remove('open'); selectedNode = null; + if (_nodePanelTrigger && typeof _nodePanelTrigger.focus === 'function') { + _nodePanelTrigger.focus(); + _nodePanelTrigger = null; + } } function chBack() { @@ -161,7 +194,7 @@ if (!text) return ''; return escapeHtml(text).replace(/@\[([^\]]+)\]/g, function(_, name) { const safeId = btoa(encodeURIComponent(name)); - return '@' + name + ''; + return '@' + name + ''; }); } @@ -223,6 +256,18 @@ closeNodeDetail(); } } + // Keyboard support for data-node elements (Bug #82) + msgEl.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { + const el = e.target.closest('[data-node]'); + if (el) { + e.preventDefault(); + const name = decodeURIComponent(atob(el.dataset.node)); + showNodeDetail(name); + } + } + }); + msgEl.addEventListener('click', handleNodeTap); // touchend fires more reliably on mobile for non-button elements let touchMoved = false; @@ -403,9 +448,9 @@ const safeId = btoa(encodeURIComponent(sender)); return `
-
${senderLetter}
+
${senderLetter}
- +
${displayText}
${meta.join(' · ')}${msg.packetId ? ` · View packet →` : ''}