From de4240d7b2f0ccc0bdfd3bff3aebdf83a1688f47 Mon Sep 17 00:00:00 2001 From: CodeAlien Date: Sat, 29 Aug 2026 09:51:04 +0000 Subject: [PATCH] feat: add keyboard navigation to static ABI explorer page Add full keyboard navigation support to dashboard/abi-explorer.html to match the accessibility baseline established in #85 for the React frontend. Changes: - Contract headers already had tabindex="0", role="button", aria-expanded, and aria-controls; Enter/Space toggle already worked. - Add ArrowRight on a contract header: opens the section and moves focus to the first fn-row. - Add ArrowLeft on a contract header: collapses the section. - Add ArrowDown/ArrowUp between contract headers. - Add ArrowDown/ArrowUp between fn-rows within an open section. - Add Escape inside a fn-list: collapse the section and return focus to the header. - Add ArrowUp on the first fn-row: return focus to the parent header. - Add tabindex="-1" to each fn-row so it can receive programmatic focus from the arrow-key handler. - Add role="rowgroup" and aria-label to fn-list containers. - Add .fn-row:focus-visible style in style.css for visible focus ring. Tests added in app.test.js (markup assertions): - tabindex="0" present on contract-header elements - role="button" present on contract-header elements - aria-expanded present on contract-header elements - aria-controls present on contract-header elements - tabindex="-1" present on fn-row elements - ArrowDown/Up/Right/Left/Escape key handling present in script - skip-link present for keyboard users Closes #[issue_id] --- dashboard/abi-explorer.html | 73 +++++++++++++++++++++++++++++++++++-- dashboard/app.test.js | 55 ++++++++++++++++++++++++++++ dashboard/style.css | 7 ++++ 3 files changed, 132 insertions(+), 3 deletions(-) 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 554ac10..898bb83 100644 --- a/dashboard/app.test.js +++ b/dashboard/app.test.js @@ -314,3 +314,58 @@ describe('fetchInvoices() error handling', () => { expect(summary.querySelectorAll('.summary-card').length).toBe(7); }); }); + +// ── 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