From d06f7c9e6e372fdf345b8d4aaffe6918e98706b8 Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Tue, 30 Dec 2025 12:24:53 +0100 Subject: [PATCH] feat: improve sidebar keyboard accessibility - Add skip-to-content link for keyboard users - Convert module headers and lesson items from divs to buttons - Add ARIA attributes (aria-expanded, aria-controls, aria-label) - Add focus return to trigger element when closing sidebar - Shift main content when sidebar is open using CSS :has() - Add code element styling in instruction boxes --- src/app.de.js | 15 +++++++++++++++ src/app.js | 15 +++++++++++++++ src/helpers/renderer.js | 19 +++++++++++++++---- src/index.de.html | 13 +++++++------ src/index.html | 13 +++++++------ 5 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/app.de.js b/src/app.de.js index 340c87d..378253d 100644 --- a/src/app.de.js +++ b/src/app.de.js @@ -67,14 +67,29 @@ let currentMode = "css"; // ================= SIDEBAR FUNCTIONS ================= +// Track element that opened sidebar for focus return +let sidebarTrigger = null; + function openSidebar() { + // Store trigger element for focus return + sidebarTrigger = document.activeElement; + elements.sidebarDrawer.classList.add("open"); elements.sidebarBackdrop.classList.add("visible"); + + // Move focus to close button for keyboard users + elements.closeSidebar.focus(); } function closeSidebar() { elements.sidebarDrawer.classList.remove("open"); elements.sidebarBackdrop.classList.remove("visible"); + + // Return focus to trigger element + if (sidebarTrigger && typeof sidebarTrigger.focus === "function") { + sidebarTrigger.focus(); + sidebarTrigger = null; + } } // ================= EXPECTED RESULT TOGGLE ================= diff --git a/src/app.js b/src/app.js index cdfaace..4e83f35 100644 --- a/src/app.js +++ b/src/app.js @@ -68,14 +68,29 @@ let currentMode = "css"; // ================= SIDEBAR FUNCTIONS ================= +// Track element that opened sidebar for focus return +let sidebarTrigger = null; + function openSidebar() { + // Store trigger element for focus return + sidebarTrigger = document.activeElement; + elements.sidebarDrawer.classList.add("open"); elements.sidebarBackdrop.classList.add("visible"); + + // Move focus to close button for keyboard users + elements.closeSidebar.focus(); } function closeSidebar() { elements.sidebarDrawer.classList.remove("open"); elements.sidebarBackdrop.classList.remove("visible"); + + // Return focus to trigger element + if (sidebarTrigger && typeof sidebarTrigger.focus === "function") { + sidebarTrigger.focus(); + sidebarTrigger = null; + } } // ================= EXPECTED RESULT TOGGLE ================= diff --git a/src/helpers/renderer.js b/src/helpers/renderer.js index e468e1e..2cab16c 100644 --- a/src/helpers/renderer.js +++ b/src/helpers/renderer.js @@ -33,11 +33,15 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes // Create module container const moduleContainer = document.createElement("div"); moduleContainer.classList.add("module-container"); + moduleContainer.setAttribute("role", "treeitem"); // Create module header item (clickable to expand/collapse) - const moduleHeader = document.createElement("div"); + const moduleHeader = document.createElement("button"); + moduleHeader.type = "button"; moduleHeader.classList.add("module-list-item", "module-header"); moduleHeader.dataset.moduleId = module.id; + moduleHeader.setAttribute("aria-expanded", "false"); + moduleHeader.setAttribute("aria-controls", `lessons-${module.id}`); // Create module title with expand/collapse indicator const moduleTitle = document.createElement("span"); @@ -47,6 +51,7 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes // Create expand/collapse icon const expandIcon = document.createElement("span"); expandIcon.classList.add("expand-icon"); + expandIcon.setAttribute("aria-hidden", "true"); expandIcon.innerHTML = "▶"; // Right-pointing triangle moduleHeader.appendChild(expandIcon); @@ -60,11 +65,15 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes // Lessons container (initially hidden) const lessonsContainer = document.createElement("div"); lessonsContainer.classList.add("lessons-container"); + lessonsContainer.id = `lessons-${module.id}`; + lessonsContainer.setAttribute("role", "group"); + lessonsContainer.setAttribute("aria-label", `${module.title} lessons`); lessonsContainer.style.display = "none"; // Initially collapsed // Create list items for each lesson in this module module.lessons.forEach((lesson, index) => { - const lessonItem = document.createElement("div"); + const lessonItem = document.createElement("button"); + lessonItem.type = "button"; lessonItem.classList.add("lesson-list-item"); lessonItem.dataset.moduleId = module.id; lessonItem.dataset.lessonIndex = index; @@ -101,8 +110,9 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes const isExpanded = lessonsContainer.style.display !== "none"; lessonsContainer.style.display = isExpanded ? "none" : "block"; - // Update expand/collapse icon + // Update expand/collapse icon and ARIA state expandIcon.innerHTML = isExpanded ? "▶" : "▼"; + moduleHeader.setAttribute("aria-expanded", String(!isExpanded)); }); // Add module header and lessons container to module container @@ -226,9 +236,10 @@ export function updateActiveLessonInSidebar(moduleId, lessonIndex) { if (parentLessonsContainer && parentLessonsContainer.classList.contains("lessons-container")) { parentLessonsContainer.style.display = "block"; - // Update expand icon + // Update expand icon and ARIA state const moduleHeader = parentLessonsContainer.previousElementSibling; if (moduleHeader) { + moduleHeader.setAttribute("aria-expanded", "true"); const expandIcon = moduleHeader.querySelector(".expand-icon"); if (expandIcon) { expandIcon.innerHTML = "▼"; // Down arrow when expanded diff --git a/src/index.de.html b/src/index.de.html index ae65182..9aa481d 100644 --- a/src/index.de.html +++ b/src/index.de.html @@ -8,6 +8,7 @@ +
@@ -27,7 +28,7 @@
-
+
@@ -97,7 +98,7 @@ -
-