From e48df3fb1e0008f14d792afa6a8a1cb7421b785f Mon Sep 17 00:00:00 2001 From: Ngo Quoc Viet <123613986+NgoQuocViet2001@users.noreply.github.com> Date: Tue, 18 Aug 2026 05:03:34 +0700 Subject: [PATCH] fix(gallery): add accessible keyboard navigation (#56) Co-authored-by: Cathryn Lavery --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- skills/diagram-design/assets/index.html | 63 +++++++++++++++++++++---- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f738221..5b4e222 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-design", "description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, or DP security matrix diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.", - "version": "2.4.2", + "version": "2.4.3", "author": { "name": "Cathryn Lavery", "url": "https://github.com/cathrynlavery" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 69cf5cf..67b3edf 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-design", "description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, or DP security matrix diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.", - "version": "2.4.2", + "version": "2.4.3", "author": { "name": "Cathryn Lavery", "url": "https://github.com/cathrynlavery" diff --git a/skills/diagram-design/assets/index.html b/skills/diagram-design/assets/index.html index b368eff..69f3341 100644 --- a/skills/diagram-design/assets/index.html +++ b/skills/diagram-design/assets/index.html @@ -194,7 +194,7 @@ -
+
@@ -311,7 +311,7 @@
@@ -334,30 +334,73 @@ const openRaw = document.getElementById("open-raw"); function update() { - const activeType = document.querySelector("#type-tabs .tab.active"); + const activeType = [...document.querySelectorAll("#type-tabs .tab")].find( + (tab) => tab.dataset.type === state.type, + ); const single = activeType && activeType.hasAttribute("data-single"); const variantTabs = document.querySelectorAll("#variant-tabs .tab"); if (single) state.variant = ""; variantTabs.forEach((tab) => { tab.disabled = single && tab.dataset.variant !== ""; - tab.classList.toggle("active", tab.dataset.variant === state.variant); }); + for (const [containerId, key] of [ + ["type-tabs", "type"], + ["variant-tabs", "variant"], + ]) { + document.querySelectorAll(`#${containerId} .tab`).forEach((tab) => { + const active = tab.dataset[key] === state[key]; + tab.classList.toggle("active", active); + tab.setAttribute("aria-checked", String(active)); + tab.tabIndex = active ? 0 : -1; + }); + } const src = `example-${state.type}${state.variant}.html`; iframe.src = src; openRaw.href = src; + const selectedTabs = document.querySelectorAll('[role="radio"][aria-checked="true"]'); + iframe.setAttribute( + "aria-labelledby", + [...selectedTabs].map((tab) => tab.id).join(" "), + ); } function bindTabs(containerId, key) { const container = document.getElementById(containerId); + const tabs = [...container.querySelectorAll(".tab")]; + tabs.forEach((tab, index) => { + tab.id = `${containerId}-${index}`; + tab.setAttribute("role", "radio"); + tab.setAttribute("aria-controls", "preview"); + }); + + function activate(tab, focus = false) { + state[key] = tab.dataset[key]; + update(); + if (focus) tab.focus(); + } + container.addEventListener("click", (e) => { const btn = e.target.closest(".tab"); if (!btn) return; - container - .querySelectorAll(".tab") - .forEach((t) => t.classList.remove("active")); - btn.classList.add("active"); - state[key] = btn.dataset[key]; - update(); + activate(btn); + }); + container.addEventListener("keydown", (e) => { + const current = e.target.closest('[role="radio"]'); + if (!current || !container.contains(current)) return; + const enabledTabs = tabs.filter((tab) => !tab.disabled); + const index = enabledTabs.indexOf(current); + let next; + if (e.key === "ArrowRight" || e.key === "ArrowDown") { + next = enabledTabs[(index + 1) % enabledTabs.length]; + } + if (e.key === "ArrowLeft" || e.key === "ArrowUp") { + next = enabledTabs[(index - 1 + enabledTabs.length) % enabledTabs.length]; + } + if (e.key === "Home") next = enabledTabs[0]; + if (e.key === "End") next = enabledTabs.at(-1); + if (!next) return; + e.preventDefault(); + activate(next, true); }); }