From f7d4d2a6b77490ce4ebb6f17e05686e4d280a89f Mon Sep 17 00:00:00 2001 From: you Date: Thu, 19 Mar 2026 15:50:18 +0000 Subject: [PATCH] fix: make table rows keyboard-accessible with delegated event listeners (fixes #9) - Replace inline onclick on elements with data-action/data-value attributes - Add tabindex="0" and role="row" to all clickable rows - Add delegated click and keydown (Enter/Space) listeners on containers - Remove window._pktSelect, _pktToggleGroup, _pktSelectHash, _nodeSelect globals - Convert to local functions referenced by delegated handlers Affected files: packets.js, nodes.js, analytics.js (channels.js and observers.js had no interactive elements) --- public/analytics.js | 20 +++++++++++++++++--- public/nodes.js | 18 +++++++++++++++--- public/packets.js | 36 +++++++++++++++++++++++++----------- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/public/analytics.js b/public/analytics.js index 576ec91..ec221c2 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -88,6 +88,20 @@ renderTab(btn.dataset.tab); }); + // Delegated click/keyboard handler for clickable table rows + const analyticsContent = document.getElementById('analyticsContent'); + if (analyticsContent) { + const handler = (e) => { + const row = e.target.closest('tr[data-action="navigate"]'); + if (!row) return; + if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; + if (e.type === 'keydown') e.preventDefault(); + location.hash = row.dataset.value; + }; + analyticsContent.addEventListener('click', handler); + analyticsContent.addEventListener('keydown', handler); + } + try { window._analyticsData = {}; const [hashData, rfData, topoData, chanData] = await Promise.all([ @@ -568,7 +582,7 @@ - ${ch.channels.map(c => ` + ${ch.channels.map(c => ` @@ -679,7 +693,7 @@
ChannelHashMessagesUnique SendersLast ActivityDecrypted
${esc(c.name || 'Unknown')} ${c.hash} ${c.messages}
- ${data.multiByteNodes.map(n => ` + ${data.multiByteNodes.map(n => ` @@ -697,7 +711,7 @@ ${data.topHops.map(h => { const link = h.pubkey ? `#/nodes/${encodeURIComponent(h.pubkey)}` : `#/packets?search=${h.hex}`; - return ` + return ` diff --git a/public/nodes.js b/public/nodes.js index 50ed95b..2dfde6d 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -257,6 +257,20 @@ th.addEventListener('click', () => { sortBy = th.dataset.sort; loadNodes(); }); }); + // Delegated click/keyboard handler for table rows + const tbody = document.getElementById('nodesBody'); + if (tbody) { + const handler = (e) => { + const row = e.target.closest('tr[data-action="select"]'); + if (!row) return; + if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; + if (e.type === 'keydown') e.preventDefault(); + selectNode(row.dataset.value); + }; + tbody.addEventListener('click', handler); + tbody.addEventListener('keydown', handler); + } + renderRows(); } @@ -271,7 +285,7 @@ tbody.innerHTML = nodes.map(n => { const roleColor = ROLE_COLORS[n.role] || '#6b7280'; - return ` + return ` @@ -407,7 +421,5 @@ return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } - window._nodeSelect = selectNode; - registerPage('nodes', { init, destroy }); })(); diff --git a/public/packets.js b/public/packets.js index a19f3ee..857aa9f 100644 --- a/public/packets.js +++ b/public/packets.js @@ -290,6 +290,24 @@ }, 250)); fNode.addEventListener('blur', () => { setTimeout(() => fNodeDrop.classList.add('hidden'), 200); }); + // Delegated click/keyboard handler for table rows + const pktBody = document.getElementById('pktBody'); + if (pktBody) { + const handler = (e) => { + const row = e.target.closest('tr[data-action]'); + if (!row) return; + if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; + if (e.type === 'keydown') e.preventDefault(); + const action = row.dataset.action; + const value = row.dataset.value; + if (action === 'select') selectPacket(Number(value)); + else if (action === 'select-hash') pktSelectHash(value); + else if (action === 'toggle-select') { pktToggleGroup(value); pktSelectHash(value); } + }; + pktBody.addEventListener('click', handler); + pktBody.addEventListener('keydown', handler); + } + renderTableRows(); makeColumnsResizable('#pktTable', 'meshcore-pkt-col-widths'); } @@ -310,10 +328,7 @@ const groupTypeClass = payloadTypeColor(p.payload_type); const groupSize = p.raw_hex ? Math.floor(p.raw_hex.length / 2) : 0; const isSingle = p.count <= 1; - const rowClick = isSingle - ? `window._pktSelectHash('${p.hash}')` - : `window._pktToggleGroup('${p.hash}'); window._pktSelectHash('${p.hash}')`; - html += ` + html += ` @@ -335,7 +350,7 @@ let childPath = []; try { childPath = JSON.parse(c.path_json || '[]'); } catch {} const childPathStr = renderPath(childPath); - html += ` + html += ` @@ -365,7 +380,7 @@ const pathStr = renderPath(pathHops); const detail = getDetailPreview(decoded); - return ` + return ` @@ -741,8 +756,7 @@ })(); // Global handlers - window._pktSelect = selectPacket; - window._pktToggleGroup = async (hash) => { + async function pktToggleGroup(hash) { if (expandedHashes.has(hash)) { expandedHashes.delete(hash); renderTableRows(); @@ -763,14 +777,14 @@ expandedHashes.add(hash); renderTableRows(); } catch {} - }; - window._pktSelectHash = async (hash) => { + } + async function pktSelectHash(hash) { // When grouped, find first packet with this hash try { const data = await api(`/packets?hash=${hash}&limit=1`); if (data.packets?.[0]) selectPacket(data.packets[0].id); } catch {} - }; + } window._pktRefresh = loadPackets; window._pktBYOP = showBYOP;
NodeHash SizeAdvertsLast Seen
${esc(n.name)} ${n.hashSize}-byte ${n.packets}
${h.hex} ${h.name ? `${esc(h.name)}` : 'unknown'} ${h.size}-byte
${favStar(n.public_key, 'node-fav')}${n.name || '(unnamed)'} ${truncate(n.public_key, 16)} ${n.role}
${isSingle ? '' : (isExpanded ? '▼' : '▶')} ${groupRegion ? `${groupRegion}` : '—'} ${timeAgo(p.latest)}
${childRegion ? `${childRegion}` : '—'} ${timeAgo(c.timestamp)} ${truncate(c.hash || '', 8)}
${region ? `${region}` : '—'} ${timeAgo(p.timestamp)} ${truncate(p.hash || String(p.id), 8)}