diff --git a/frontend/src/components/Chat/ConversationPanel.styles.ts b/frontend/src/components/Chat/ConversationPanel.styles.ts index b8c0daf98b..d821b33580 100644 --- a/frontend/src/components/Chat/ConversationPanel.styles.ts +++ b/frontend/src/components/Chat/ConversationPanel.styles.ts @@ -41,6 +41,10 @@ export const useConversationPanelStyles = makeStyles({ '&:hover': { backgroundColor: tokens.colorNeutralBackground1Hover, }, + '&:focus-visible': { + outline: `2px solid ${tokens.colorStrokeFocus2}`, + outlineOffset: '-2px', + }, }, conversationItemActive: { backgroundColor: tokens.colorNeutralBackground1Selected, diff --git a/frontend/src/components/Chat/ConversationPanel.test.tsx b/frontend/src/components/Chat/ConversationPanel.test.tsx index d7c8f134bb..286a23ed63 100644 --- a/frontend/src/components/Chat/ConversationPanel.test.tsx +++ b/frontend/src/components/Chat/ConversationPanel.test.tsx @@ -217,6 +217,46 @@ describe("ConversationPanel", () => { expect(onSelectConversation).toHaveBeenCalledWith("branch-1"); }); + it("should be keyboard accessible for conversation selection", async () => { + const user = userEvent.setup(); + const onSelectConversation = jest.fn(); + + mockedAttacksApi.getConversations.mockResolvedValue({ + attack_result_id: "ar-attack-123", + main_conversation_id: "attack-123", + conversations: [ + { + conversation_id: "branch-1", + message_count: 2, + last_message_preview: null, + created_at: "2026-02-18T11:00:00Z", + }, + ], + }); + + render( + + + + ); + + const convItem = await screen.findByTestId("conversation-item-branch-1"); + expect(convItem).toHaveAttribute("role", "button"); + expect(convItem).toHaveAttribute("tabindex", "0"); + + convItem.focus(); + await user.keyboard("{Enter}"); + expect(onSelectConversation).toHaveBeenCalledWith("branch-1"); + + onSelectConversation.mockClear(); + convItem.focus(); + await user.keyboard(" "); + expect(onSelectConversation).toHaveBeenCalledWith("branch-1"); + }); + it("should call onNewConversation when clicking new conversation button", async () => { const user = userEvent.setup(); const onNewConversation = jest.fn(); diff --git a/frontend/src/components/Chat/ConversationPanel.tsx b/frontend/src/components/Chat/ConversationPanel.tsx index 569ead5763..a49b9e3c32 100644 --- a/frontend/src/components/Chat/ConversationPanel.tsx +++ b/frontend/src/components/Chat/ConversationPanel.tsx @@ -196,11 +196,22 @@ export default function ConversationPanel({ {!isLoading && !error && conversations.map((conv) => { const isActive = conv.conversation_id === activeConversationId + const selectConversation = () => onSelectConversation(conv.conversation_id) return (
onSelectConversation(conv.conversation_id)} + onClick={selectConversation} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + selectConversation() + } + }} + role="button" + tabIndex={0} + aria-label={`Select conversation ${conv.conversation_id}`} + aria-current={isActive ? 'true' : undefined} data-testid={`conversation-item-${conv.conversation_id}`} >
diff --git a/frontend/src/components/History/AttackHistory.styles.ts b/frontend/src/components/History/AttackHistory.styles.ts index 6dcafd8634..7c0c87417c 100644 --- a/frontend/src/components/History/AttackHistory.styles.ts +++ b/frontend/src/components/History/AttackHistory.styles.ts @@ -53,6 +53,10 @@ export const useAttackHistoryStyles = makeStyles({ ':hover': { backgroundColor: tokens.colorNeutralBackground1Hover, }, + ':focus-visible': { + outline: `2px solid ${tokens.colorStrokeFocus2}`, + outlineOffset: '-2px', + }, }, previewCell: { display: 'block', diff --git a/frontend/src/components/History/AttackTable.test.tsx b/frontend/src/components/History/AttackTable.test.tsx index eefe89dda5..03f7ec6063 100644 --- a/frontend/src/components/History/AttackTable.test.tsx +++ b/frontend/src/components/History/AttackTable.test.tsx @@ -1,4 +1,5 @@ import { render, screen, fireEvent } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { FluentProvider, webLightTheme } from '@fluentui/react-components' import AttackTable from './AttackTable' import type { AttackSummary } from '../../types' @@ -145,6 +146,34 @@ describe('AttackTable', () => { expect(onOpenAttack).toHaveBeenCalledWith('ar-1') }) + it('should call onOpenAttack when Enter or Space is pressed on a row', async () => { + const user = userEvent.setup() + const onOpenAttack = jest.fn() + + render( + + + + ) + + const row = screen.getByTestId('attack-row-ar-1') + expect(row).toHaveAttribute('tabindex', '0') + + row.focus() + await user.keyboard('{Enter}') + expect(onOpenAttack).toHaveBeenCalledWith('ar-1') + + onOpenAttack.mockClear() + row.focus() + await user.keyboard(' ') + expect(onOpenAttack).toHaveBeenCalledWith('ar-1') + + onOpenAttack.mockClear() + row.focus() + await user.keyboard('a') + expect(onOpenAttack).not.toHaveBeenCalled() + }) + it('should call onOpenAttack when open button is clicked', () => { const onOpenAttack = jest.fn() diff --git a/frontend/src/components/History/AttackTable.tsx b/frontend/src/components/History/AttackTable.tsx index 16d052d7c7..906be3538b 100644 --- a/frontend/src/components/History/AttackTable.tsx +++ b/frontend/src/components/History/AttackTable.tsx @@ -69,6 +69,14 @@ export default function AttackTable({ attacks, onOpenAttack, formatDate }: Attac key={attack.attack_result_id} className={styles.clickableRow} onClick={() => onOpenAttack(attack.attack_result_id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onOpenAttack(attack.attack_result_id) + } + }} + tabIndex={0} + aria-label={`Open ${attack.attack_type} attack`} data-testid={`attack-row-${attack.attack_result_id}`} >