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"); + }); + }); + }); +});