From fe93d64fc9903c1c6b2ca7159ec3333931504e87 Mon Sep 17 00:00:00 2001 From: pooramara Date: Tue, 20 Oct 2026 16:42:15 +0100 Subject: [PATCH] feat: a11y statement, mobile nav, module TOC, changelog link (#116-#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #116: add accessibility statement dialog with WCAG 2.1 AA info and feedback contact — accessible from footer and mobile menu - #117: add compact mobile navigation menu with accessible toggle (aria-expanded, aria-controls, keyboard-accessible) - #118: add table of contents for the module section with expand/collapse and smooth-scroll links to each module card - #119: add changelog link to the page footer pointing to GitHub releases --- src/App.test.tsx | 83 ++++++++++++++++++++++++++++++++++++ src/App.tsx | 92 +++++++++++++++++++++++++++++++++++++++- src/styles.css | 108 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 281 insertions(+), 2 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 9c9efd2..233669a 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -708,3 +708,86 @@ describe("Navigation enhancements (#112, #113, #114, #115)", () => { expect(html).toHaveStyle("scroll-padding-top: 78px"); }); }); + +// #117 — mobile navigation menu +describe("Mobile navigation menu", () => { + it("renders the mobile menu toggle button", () => { + render(); + expect(screen.getByRole("button", { name: /open navigation menu/i })).toBeInTheDocument(); + }); + + it("toggles the mobile menu on click", () => { + render(); + const toggle = screen.getByRole("button", { name: /open navigation menu/i }); + fireEvent.click(toggle); + expect(toggle).toHaveAttribute("aria-expanded", "true"); + expect(screen.getByRole("button", { name: /close navigation menu/i })).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: /close navigation menu/i })); + expect(screen.getByRole("button", { name: /open navigation menu/i })).toHaveAttribute("aria-expanded", "false"); + }); + + it("has accessible aria attributes on the toggle", () => { + render(); + const toggle = screen.getByRole("button", { name: /open navigation menu/i }); + expect(toggle).toHaveAttribute("aria-controls", "mobile-menu"); + }); +}); + +// #118 — module table of contents +describe("Module table of contents", () => { + it("renders the TOC toggle button", () => { + render(); + expect(screen.getByRole("button", { name: /show table of contents/i })).toBeInTheDocument(); + }); + + it("expands the TOC list on click", () => { + render(); + const toggle = screen.getByRole("button", { name: /show table of contents/i }); + fireEvent.click(toggle); + expect(toggle).toHaveAttribute("aria-expanded", "true"); + expect(screen.getByRole("navigation", { name: /module table of contents/i })).toBeInTheDocument(); + }); + + it("lists all modules in the TOC", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: /show table of contents/i })); + const tocList = document.getElementById("module-toc-list"); + expect(tocList).toBeInTheDocument(); + expect(tocList!.querySelectorAll("li").length).toBeGreaterThan(0); + }); +}); + +// #119 — changelog link in footer +describe("Footer changelog link", () => { + it("renders a changelog link in the footer", () => { + render(); + const footer = screen.getByRole("contentinfo"); + const changelogLink = within(footer).getByRole("link", { name: /changelog/i }); + expect(changelogLink).toHaveAttribute("href", expect.stringContaining("/releases")); + }); +}); + +// #116 — accessibility statement +describe("Accessibility statement", () => { + it("renders the accessibility link in the footer", () => { + render(); + const footer = screen.getByRole("contentinfo"); + expect(within(footer).getByRole("button", { name: /accessibility statement/i })).toBeInTheDocument(); + }); + + it("opens the accessibility dialog on click", () => { + render(); + const footer = screen.getByRole("contentinfo"); + fireEvent.click(within(footer).getByRole("button", { name: /accessibility statement/i })); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText(/accessibility statement/i)).toBeInTheDocument(); + }); + + it("closes the dialog when the close button is clicked", () => { + render(); + const footer = screen.getByRole("contentinfo"); + fireEvent.click(within(footer).getByRole("button", { name: /accessibility statement/i })); + fireEvent.click(screen.getByRole("button", { name: /^close$/i })); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); +}); diff --git a/src/App.tsx b/src/App.tsx index de74841..57186b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -257,6 +257,16 @@ export default function App() { const [auditGuideOpen, setAuditGuideOpen] = useState(false); const [checkedPrerequisites, setCheckedPrerequisites] = useState>({}); const comparePanelRef = useRef(null); + const [showBackToTop, setShowBackToTop] = useState(false); + const [categoryFilter, setCategoryFilter] = useState(null); + const [selectedModule, setSelectedModule] = useState(null); + // #117 — mobile nav toggle + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const mobileMenuRef = useRef(null); + // #118 — module table of contents + const [tocOpen, setTocOpen] = useState(false); + // #116 — accessibility statement modal + const [a11yOpen, setA11yOpen] = useState(false); const [activeSection, setActiveSection] = useActiveSection(navSectionIds); useEffect(() => { @@ -493,19 +503,34 @@ export default function App() { S Soroban Testkit -
+ {/* #117 — accessible mobile nav toggle */} + +
{navSections.map((section) => ( setActiveSection(section.id)} + onClick={() => { setActiveSection(section.id); setMobileMenuOpen(false); }} onFocus={() => setActiveSection(section.id)} > {section.label} ))} GitHub ↗ + {/* #116 — accessibility statement link */} +
@@ -603,6 +628,35 @@ export default function App() {

One prelude. Seven focused modules.

Everything a serious contract test needs.

+ {/* #118 — module table of contents */} +