diff --git a/src/components/AIChatPanel.tsx b/src/components/AIChatPanel.tsx
index f422f27b..7de30964 100644
--- a/src/components/AIChatPanel.tsx
+++ b/src/components/AIChatPanel.tsx
@@ -400,7 +400,17 @@ export const AIChatPanel = () => {
Context:
{/* TemplateMark Button */}
handleTemplateMarkToggle(!includeTemplateMarkContent)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleTemplateMarkToggle(!includeTemplateMarkContent);
+ }
+ }}
className={
`px-1 py-0.5 text-xs rounded-full flex items-center cursor-pointer border transition-colors
${includeTemplateMarkContent
@@ -435,7 +445,17 @@ export const AIChatPanel = () => {
{/* Concerto Button */}
handleConcertoModelToggle(!includeConcertoModelContent)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleConcertoModelToggle(!includeConcertoModelContent);
+ }
+ }}
className={
`px-1 py-0.5 text-xs rounded-full flex items-center cursor-pointer border transition-colors
${includeConcertoModelContent
@@ -470,7 +490,17 @@ export const AIChatPanel = () => {
{/* Data Button */}
handleDataToggle(!includeDataContent)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleDataToggle(!includeDataContent);
+ }
+ }}
className={
`px-1 py-0.5 text-xs rounded-full flex items-center cursor-pointer border transition-colors
${includeDataContent
diff --git a/src/components/FullScreenModal.tsx b/src/components/FullScreenModal.tsx
index b4c1fdd3..9174c627 100644
--- a/src/components/FullScreenModal.tsx
+++ b/src/components/FullScreenModal.tsx
@@ -38,8 +38,17 @@ const FullScreenModal: React.FC = () => {
<>
setOpen(true)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setOpen(true);
+ }
+ }}
/>
{
aria-label={title}
tabIndex={0}
onClick={onClick}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onClick?.();
+ }
+ }}
className={`group playground-sidebar-nav-item ${
active ? 'playground-sidebar-nav-item-active' : 'playground-sidebar-nav-item-inactive'
} tour-${title.toLowerCase().replace(' ', '-')}`}
@@ -188,6 +194,12 @@ const PlaygroundSidebar = () => {
aria-label={title}
tabIndex={0}
onClick={onClick}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onClick();
+ }
+ }}
className={`group playground-sidebar-nav-bottom-item tour-${title.toLowerCase().replace(' ', '-')}`}
>
diff --git a/src/tests/components/KeyboardAccessibility.test.tsx b/src/tests/components/KeyboardAccessibility.test.tsx
new file mode 100644
index 00000000..c52e80ea
--- /dev/null
+++ b/src/tests/components/KeyboardAccessibility.test.tsx
@@ -0,0 +1,124 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import PlaygroundSidebar from "../../components/PlaygroundSidebar";
+import { vi } from "vitest";
+
+// Mock the store
+const mockSetEditorsVisible = vi.fn();
+const mockSetPreviewVisible = vi.fn();
+const mockSetProblemPanelVisible = vi.fn();
+const mockSetAIChatOpen = vi.fn();
+const mockSetSettingsOpen = vi.fn();
+
+vi.mock("../../store/store", () => ({
+ default: () => ({
+ isEditorsVisible: true,
+ isPreviewVisible: true,
+ isProblemPanelVisible: false,
+ isAIChatOpen: false,
+ setEditorsVisible: mockSetEditorsVisible,
+ setPreviewVisible: mockSetPreviewVisible,
+ setProblemPanelVisible: mockSetProblemPanelVisible,
+ setAIChatOpen: mockSetAIChatOpen,
+ setSettingsOpen: mockSetSettingsOpen,
+ generateShareableLink: vi.fn(() => "https://example.com"),
+ textColor: "#000000",
+ backgroundColor: "#ffffff",
+ }),
+}));
+
+vi.mock("../../components/Tour", () => ({
+ default: { start: vi.fn() },
+}));
+
+vi.mock("../../components/FullScreenModal", () => ({
+ default: () => FullScreen
,
+}));
+
+vi.mock("../../components/SettingsModal", () => ({
+ default: () => Settings
,
+}));
+
+describe("Keyboard Accessibility", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("PlaygroundSidebar - Top Nav", () => {
+ it("activates Editor button on Enter key", () => {
+ render();
+ const editorButton = screen.getByRole("button", { name: /Editor/i });
+ fireEvent.keyDown(editorButton, { key: "Enter" });
+ expect(mockSetEditorsVisible).toHaveBeenCalled();
+ });
+
+ it("activates Editor button on Space key", () => {
+ render();
+ const editorButton = screen.getByRole("button", { name: /Editor/i });
+ fireEvent.keyDown(editorButton, { key: " " });
+ expect(mockSetEditorsVisible).toHaveBeenCalled();
+ });
+
+ it("does not activate Editor button on other keys", () => {
+ render();
+ const editorButton = screen.getByRole("button", { name: /Editor/i });
+ fireEvent.keyDown(editorButton, { key: "Tab" });
+ expect(mockSetEditorsVisible).not.toHaveBeenCalled();
+ });
+
+ it("activates Preview button on Enter key", () => {
+ render();
+ const previewButton = screen.getByRole("button", { name: /Preview/i });
+ fireEvent.keyDown(previewButton, { key: "Enter" });
+ expect(mockSetPreviewVisible).toHaveBeenCalled();
+ });
+
+ it("activates Problems button on Space key", () => {
+ render();
+ const problemsButton = screen.getByRole("button", { name: /Problems/i });
+ fireEvent.keyDown(problemsButton, { key: " " });
+ expect(mockSetProblemPanelVisible).toHaveBeenCalled();
+ });
+
+ it("activates AI Assistant button on Enter key", () => {
+ render();
+ const aiButton = screen.getByRole("button", { name: /AI Assistant/i });
+ fireEvent.keyDown(aiButton, { key: "Enter" });
+ expect(mockSetAIChatOpen).toHaveBeenCalled();
+ });
+ });
+
+ describe("PlaygroundSidebar - Bottom Nav", () => {
+ it("activates Share button on Enter key", () => {
+ render();
+ const shareButton = screen.getByRole("button", { name: /Share/i });
+ fireEvent.keyDown(shareButton, { key: "Enter" });
+ // Share calls handleShare which is async, just verify no error
+ expect(shareButton).toBeInTheDocument();
+ });
+
+ it("activates Settings button on Space key", () => {
+ render();
+ const settingsButton = screen.getByRole("button", { name: /Settings/i });
+ fireEvent.keyDown(settingsButton, { key: " " });
+ expect(mockSetSettingsOpen).toHaveBeenCalledWith(true);
+ });
+
+ it("activates Start Tour button on Enter key", () => {
+ render();
+ const tourButton = screen.getByRole("button", { name: /Start Tour/i });
+ fireEvent.keyDown(tourButton, { key: "Enter" });
+ expect(tourButton).toBeInTheDocument();
+ });
+ });
+
+ describe("PlaygroundSidebar - focusability", () => {
+ it("all nav items have tabIndex=0 for keyboard focus", () => {
+ render();
+ const buttons = screen.getAllByRole("button");
+ buttons.forEach((button) => {
+ expect(button).toHaveAttribute("tabindex", "0");
+ });
+ });
+ });
+});