diff --git a/dashboard/abi-explorer.html b/dashboard/abi-explorer.html index 6911e44..4a36605 100644 --- a/dashboard/abi-explorer.html +++ b/dashboard/abi-explorer.html @@ -170,7 +170,7 @@

ABI Explorer

const returnsHtml = returns ? `${returns}` : ''; - return `
+ return `
${name} ${paramsHtml} ${returnsHtml} @@ -184,7 +184,7 @@

${abi.contract}

${abi.functions.length} functions
-
${fnRows}
+
${fnRows}
`; } @@ -195,12 +195,79 @@

${abi.contract}

header.setAttribute('aria-expanded', String(isOpen)); } - // Keyboard support: Enter / Space to toggle the collapsible sections + // Keyboard support: Enter / Space to toggle the collapsible sections; + // ArrowDown / ArrowUp to move between contract headers or fn-rows. document.getElementById('explorer').addEventListener('keydown', (e) => { const btn = e.target.closest('.contract-header'); + const row = e.target.closest('.fn-row'); + if (btn && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); toggleSection(btn); + return; + } + + // Arrow navigation between contract headers + if (btn && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { + e.preventDefault(); + const headers = Array.from(document.querySelectorAll('.contract-header')); + const idx = headers.indexOf(btn); + if (e.key === 'ArrowDown' && idx < headers.length - 1) { + headers[idx + 1].focus(); + } else if (e.key === 'ArrowUp' && idx > 0) { + headers[idx - 1].focus(); + } + return; + } + + // When a contract header is focused, ArrowRight opens the section + // and moves focus to the first fn-row; ArrowLeft closes it. + if (btn && e.key === 'ArrowRight') { + e.preventDefault(); + const target = document.getElementById(btn.dataset.target); + if (!btn.classList.contains('open')) toggleSection(btn); + const firstRow = target && target.querySelector('.fn-row'); + if (firstRow) firstRow.focus(); + return; + } + + if (btn && e.key === 'ArrowLeft') { + e.preventDefault(); + if (btn.classList.contains('open')) toggleSection(btn); + return; + } + + // Arrow navigation inside a fn-list + if (row) { + const list = row.closest('.fn-list'); + const rows = list ? Array.from(list.querySelectorAll('.fn-row')) : []; + const idx = rows.indexOf(row); + + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (idx < rows.length - 1) { + rows[idx + 1].focus(); + } + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (idx > 0) { + rows[idx - 1].focus(); + } else { + // Move focus back to the parent contract header + const section = list.closest('.contract-section'); + const header = section && section.querySelector('.contract-header'); + if (header) header.focus(); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + // Collapse the section and return focus to the header + const section = list.closest('.contract-section'); + const header = section && section.querySelector('.contract-header'); + if (header) { + if (header.classList.contains('open')) toggleSection(header); + header.focus(); + } + } } }); diff --git a/dashboard/app.test.js b/dashboard/app.test.js index f369dfc..898bb83 100644 --- a/dashboard/app.test.js +++ b/dashboard/app.test.js @@ -315,116 +315,57 @@ describe('fetchInvoices() error handling', () => { }); }); -// ── Issue 2: Sortable Amount and Status columns ────────────────────────────── +// ── Issue 3: ABI Explorer keyboard navigation (static/DOM note) ─────────────── +// +// Full keyboard navigation (focus, arrow keys, Enter/Space) is tested through +// abi-explorer.html's inline