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