Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/components/AIChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,17 @@ export const AIChatPanel = () => {
<span className="text-xs text-gray-600 mr-1" style={{color: textColor}}>Context:</span>
{/* TemplateMark Button */}
<div
role="button"
tabIndex={0}
aria-label="Toggle TemplateMark context"
aria-pressed={includeTemplateMarkContent}
onClick={() => 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
Expand Down Expand Up @@ -435,7 +445,17 @@ export const AIChatPanel = () => {
</div>
{/* Concerto Button */}
<div
role="button"
tabIndex={0}
aria-label="Toggle Concerto context"
aria-pressed={includeConcertoModelContent}
onClick={() => 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
Expand Down Expand Up @@ -470,7 +490,17 @@ export const AIChatPanel = () => {
</div>
{/* Data Button */}
<div
role="button"
tabIndex={0}
aria-label="Toggle Data context"
aria-pressed={includeDataContent}
onClick={() => 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
Expand Down
9 changes: 9 additions & 0 deletions src/components/FullScreenModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,17 @@ const FullScreenModal: React.FC = () => {
<>
<MdFullscreen
size={24}
role="button"
tabIndex={0}
aria-label="Open fullscreen preview"
style={{ cursor: "pointer" }}
onClick={() => setOpen(true)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setOpen(true);
}
}}
Comment on lines +41 to +51
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FullScreenModal's MdFullscreen icon now has role="button", tabIndex={0}, and aria-label attributes, but it is rendered inside the "Fullscreen" nav item container in PlaygroundSidebar, which also has role="button" and tabIndex={0}. This creates a nested interactive elements structure — a role="button" element inside another role="button" — which is invalid per the ARIA spec (interactive controls cannot be nested inside each other). Screen readers and assistive technologies may handle this unpredictably.

Suggested change
role="button"
tabIndex={0}
aria-label="Open fullscreen preview"
style={{ cursor: "pointer" }}
onClick={() => setOpen(true)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setOpen(true);
}
}}
style={{ cursor: "pointer" }}
onClick={() => setOpen(true)}

Copilot uses AI. Check for mistakes.
/>
<Modal
title="Output"
Expand Down
12 changes: 12 additions & 0 deletions src/components/PlaygroundSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,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-item ${
active ? 'playground-sidebar-nav-item-active' : 'playground-sidebar-nav-item-inactive'
} tour-${title.toLowerCase().replace(' ', '-')}`}
Expand All @@ -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(' ', '-')}`}
>
<Icon size={18} />
Expand Down
124 changes: 124 additions & 0 deletions src/tests/components/KeyboardAccessibility.test.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <div data-testid="fullscreen-modal">FullScreen</div>,
}));

vi.mock("../../components/SettingsModal", () => ({
default: () => <div data-testid="settings-modal">Settings</div>,
}));

describe("Keyboard Accessibility", () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe("PlaygroundSidebar - Top Nav", () => {
it("activates Editor button on Enter key", () => {
render(<PlaygroundSidebar />);
const editorButton = screen.getByRole("button", { name: /Editor/i });
fireEvent.keyDown(editorButton, { key: "Enter" });
expect(mockSetEditorsVisible).toHaveBeenCalled();
});

it("activates Editor button on Space key", () => {
render(<PlaygroundSidebar />);
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(<PlaygroundSidebar />);
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(<PlaygroundSidebar />);
const previewButton = screen.getByRole("button", { name: /Preview/i });
fireEvent.keyDown(previewButton, { key: "Enter" });
expect(mockSetPreviewVisible).toHaveBeenCalled();
});

it("activates Problems button on Space key", () => {
render(<PlaygroundSidebar />);
const problemsButton = screen.getByRole("button", { name: /Problems/i });
fireEvent.keyDown(problemsButton, { key: " " });
expect(mockSetProblemPanelVisible).toHaveBeenCalled();
});

it("activates AI Assistant button on Enter key", () => {
render(<PlaygroundSidebar />);
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(<PlaygroundSidebar />);
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(<PlaygroundSidebar />);
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(<PlaygroundSidebar />);
const tourButton = screen.getByRole("button", { name: /Start Tour/i });
fireEvent.keyDown(tourButton, { key: "Enter" });
expect(tourButton).toBeInTheDocument();
});
Comment on lines +92 to +112
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests for "Share" (lines 92-98) and "Start Tour" (lines 107-112) fire keyDown events but their assertions only verify that the button element is in the document (expect(button).toBeInTheDocument()). This does not actually verify that the keyboard handler was triggered or that any action occurred. Such tests will always pass regardless of whether the onKeyDown handler works or not.

For the "Share" test, navigator.clipboard.writeText could be mocked to verify it was called. For the "Start Tour" test, the Tour mock is already set up and Tour.start could be asserted to have been called (vi.mock("../../components/Tour", ...)). At minimum, the comment "Share calls handleShare which is async, just verify no error" acknowledges this but the same weak pattern is applied to the Tour test without explanation.

Copilot generated this review using guidance from repository custom instructions.
});

describe("PlaygroundSidebar - focusability", () => {
it("all nav items have tabIndex=0 for keyboard focus", () => {
render(<PlaygroundSidebar />);
const buttons = screen.getAllByRole("button");
buttons.forEach((button) => {
expect(button).toHaveAttribute("tabindex", "0");
});
});
});
});
Comment on lines +42 to +124
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyboard accessibility changes for AIChatPanel (context toggle buttons) and FullScreenModal (fullscreen trigger) are not covered by any unit tests. The new test file only tests PlaygroundSidebar keyboard accessibility. Given that the PR description states "Vital features and changes captured in unit and/or integration tests" is unchecked in the author checklist, and that the codebase has unit tests for components like SettingsModal and PlaygroundSidebar, tests for the onKeyDown handlers in AIChatPanel and FullScreenModal should be added.

Copilot generated this review using guidance from repository custom instructions.
Loading