diff --git a/canvas/src/components/ThemeToggle.tsx b/canvas/src/components/ThemeToggle.tsx index c99519b8..322ff3df 100644 --- a/canvas/src/components/ThemeToggle.tsx +++ b/canvas/src/components/ThemeToggle.tsx @@ -1,6 +1,7 @@ "use client"; import { useTheme, type ThemePreference } from "@/lib/theme-provider"; +import { useCallback } from "react"; const OPTIONS: { value: ThemePreference; label: string; icon: string }[] = [ // Sun: explicit light @@ -33,17 +34,47 @@ const OPTIONS: { value: ThemePreference; label: string; icon: string }[] = [ * * Aligned with molecule-app/components/theme-toggle.tsx so the picker * behaves identically across surfaces. + * + * WCAG 2.4.7: focus-visible rings on all three icon buttons. + * ARIA radiogroup pattern (2.1.1): Left/Right arrow keys move focus + * between options and update selection; Home/End jump to first/last. */ export function ThemeToggle({ className = "" }: { className?: string }) { const { theme, setTheme } = useTheme(); + const handleKeyDown = useCallback( + (e: React.KeyboardEvent, index: number) => { + let next = index; + if (e.key === "ArrowRight" || e.key === "ArrowDown") { + e.preventDefault(); + next = (index + 1) % OPTIONS.length; + } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { + e.preventDefault(); + next = (index - 1 + OPTIONS.length) % OPTIONS.length; + } else if (e.key === "Home") { + e.preventDefault(); + next = 0; + } else if (e.key === "End") { + e.preventDefault(); + next = OPTIONS.length - 1; + } else { + return; + } + setTheme(OPTIONS[next].value); + // Move focus to the new button so arrow-key navigation is continuous + const btns = (e.currentTarget.closest("[role=radiogroup]") as HTMLElement)?.querySelectorAll("[role=radio]"); + btns?.[next]?.focus(); + }, + [] + ); + return (
- {OPTIONS.map((opt) => { + {OPTIONS.map((opt, index) => { const active = theme === opt.value; return (